package com.blogspot.radialmind;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;

public class GenerateSarReport 
{
	private VersionInfo versionInfo = null;
	private static List<Invocation> invocations = new ArrayList<Invocation>();
	private static List<String> colors = new ArrayList<String>();
	
	static {
		// These are the types of reports that sar offers
		invocations.add( new Invocation( "-b", "I_O transfer rate" ) );
		invocations.add( new Invocation( "-B", "Paging statistics" ) );
		invocations.add( new Invocation( "-d -p", "Block device activity" ) );
		invocations.add( new Invocation( "-I SUM -I ALL", "Interrupt statistics" ) );
		invocations.add( new Invocation( "-m", "Power management statistics" ) );
		invocations.add( new Invocation( "-n DEV", "Network device statistics" ) );
		invocations.add( new Invocation( "-n EDEV", "Network device errors" ) );
		invocations.add( new Invocation( "-n NFS", "NFS client statistics" ) );
		invocations.add( new Invocation( "-n NFSD", "NFS server statistics" ) );
		invocations.add( new Invocation( "-n SOCK", "Socket statistics" ) );
		invocations.add( new Invocation( "-n IP", "IP statistics" ) );
		invocations.add( new Invocation( "-n EIP", "IP errors" ) );
		invocations.add( new Invocation( "-n ICMP", "ICMP statistics" ) );
		invocations.add( new Invocation( "-n EICMP", "ICMP errors" ) );
		invocations.add( new Invocation( "-n TCP", "TCP statistics" ) );
		invocations.add( new Invocation( "-n ETCP", "TCP errors" ) );
		invocations.add( new Invocation( "-n UDP", "UDP statistics" ) );
		invocations.add( new Invocation( "-n SOCK6", "IPv6 socket statistics" ) );
		invocations.add( new Invocation( "-n IP6", "IPv6 statistics" ) );
		invocations.add( new Invocation( "-n EIP6", "IPv6 errors" ) );
		invocations.add( new Invocation( "-n ICMP6", "ICMP IPv6 statistics" ) );
		invocations.add( new Invocation( "-n EICMP6", "ICMP IPv6 errors" ) );
		invocations.add( new Invocation( "-n UDP6", "UDP IPv6 statistics" ) );
		invocations.add( new Invocation( "-P ALL", "Processor statistics" ) );
		invocations.add( new Invocation( "-q", "Queue length and load averages" ) );
		invocations.add( new Invocation( "-r", "Memory utilization" ) );
		invocations.add( new Invocation( "-R", "Memory statistics" ) );
		invocations.add( new Invocation( "-S", "Swap space utilization" ) );
		invocations.add( new Invocation( "-u ALL", "Processor utilization" ) );
		invocations.add( new Invocation( "-v", "Status inode file + kernel tables" ) );
		invocations.add( new Invocation( "-w", "Task creation and system switching activity" ) );
		invocations.add( new Invocation( "-W", "Swapping statistics" ) );
		invocations.add( new Invocation( "-y", "TTY device activity" ) );
		
		// A couple of HTML colors for use.
		colors.add( "#008000" );
		colors.add( "#FF0000" );
		colors.add( "#00FFFF" );
		colors.add( "#000080" );
		colors.add( "#FF00FF" );
		colors.add( "#808000" );
		colors.add( "#E9967A" );
		colors.add( "#800000" );
		colors.add( "#808080" );
		colors.add( "#00FF00" );
		colors.add( "#008080" );
		colors.add( "#800080" );
		colors.add( "#000000" );
		colors.add( "#FF69B4" );
		colors.add( "#BDB76B" );
		colors.add( "#9ACD32" );
		colors.add( "#FFFF00" );
	};
	
	private static void printUsage() {
		System.out.println("java -jar performance-report-<version>.jar <output-dir> <filename> <interval>\n" );
		System.out.println("<version>: The version of the performance report jar file (given)");
		System.out.println("<output-dir>: The directory where rrd and png files will be stored");
		System.out.println("<filename>: The filename of the SADC file that you're analyzing");
		System.out.println("<interval>: The interval with which samples are made by sar");
	}
	
	// The buffer is used to add characters to during parsing.
	private StringBuilder buffer = new StringBuilder();
	
	// The terms are all terms parsed from a single line of sar output.
	private List<ITerm> terms = new ArrayList<ITerm>();
	
	// The collection of graphs that we're building up. graphs.size() == invocations.size()
	private List<IGraph> graphs = new ArrayList<IGraph>();
	
	// The current graph being worked on.
	private IGraph curGraph;
	
	// The title that should be given to the graph file. Comes from "Invocation".
	private String curTitle;
	
	// The directory where files are output. Supplied by cmd argument.
	private String outputDir;
	
	// This records the time in a buffer for a single data line.
	private StringBuffer time = new StringBuffer();
	
	// The current date, parsed from the start of the sar output.
	private String curDate = null;
	
	// The interval with which samples are taken by sar
	private String interval = null;
	
	// The title of the HTML page.
	private String title = null;
	
	// Remove invalid or not-so-handy characters from filename.
	private String sanitizeFileName( String fileName ) {
		fileName = fileName.replace( " ", "_" );
		fileName = fileName.replace( "/", "_" );
		return fileName;
	}
	
	// Appends a character to the buffer. Called by the parser.
	public void appendChar( char c ) {
		buffer.append( c );
	}
	
	// When a word is terminated, adds what is in  the buffer and prepares for the next run.
	public void addWord() {
		terms.add( new StringTerm( buffer.toString() ));
		clearBuffer();
	}
	
	// Parses the date at the header line of the sar output.
	public void addDate() {
		curDate = buffer.toString();
		
		DateFormat dfm = new SimpleDateFormat("MM/dd/yyyy");
		dfm.setTimeZone(TimeZone.getTimeZone("Europe/Amsterdam"));
		
		Date a = null;
		try {
			a = dfm.parse( curDate.toString() );
		} catch (ParseException e) {
			e.printStackTrace();
			a = new Date();
		}
		terms.add( new DateTerm( a ));	

		clearBuffer();
	}

	// Parses the time at the start of a sar data line.
	public void addTime() {
		time.append( curDate );
		time.append( " " );
		time.append( buffer.toString() );
		time.append( " " );
		clearBuffer();
	}
	
	// Parses the PM or AM capitals and sets up the time now that all is known about it.
	public void addPmAm() {
		time.append( buffer.toString() );
		
		DateFormat dfm = new SimpleDateFormat("MM/dd/yyyy hh:mm:ss a");
		dfm.setTimeZone(TimeZone.getTimeZone("Europe/Amsterdam"));
		
		terms.add( new StringTerm( time.toString() ) );
		Date a = null;
		try {
			a = dfm.parse( time.toString() );
		} catch (ParseException e) {
			e.printStackTrace();
			a = new Date();
		}
		terms.add( new DateTerm( a ));		
		clearBuffer();
	}
	
	// Adds an integer read by the parser.
	public void addInteger() {
		terms.add( new IntegerTerm( buffer.toString() ));
		clearBuffer();
	}
	
	// Attempts to add a float read by the parser.
	public void addFloat() {
		terms.add( new FloatTerm( buffer.toString() ));
		clearBuffer();
	}
	
	// Verifies and stores version info from the sar output header line. 
	public void verifyAndStoreVersions() {	
		versionInfo = new VersionInfo( 
			terms.get( 0 ).toString(),
			terms.get( 1 ).toString(),
			terms.get( 2 ).toString(),
			terms.get( 3 ).toString(),
			terms.get( 4 ).toString(),
			terms.get( 5 ).toInteger() );
		curGraph = null;
	} 
	
	// The header of some data lines indicates the type of graph that should be created.
	// In this implementation, whenever XX:XX:XX PM|AM  SOMETHINGINALLCAPS  %xxx xxxX/s is seen,
	// then it is a multigraph, because the third column in the data describes an ID for a device
	// that is being analyzed. (interrupt X, cpu X, etc.). So the line detects multigraphs by the
	// ALLCAPS term at the third location in a header row.
	public void prepareMultiGraphHeader() {
		if ( curGraph == null ) {
			curGraph = new MultiGraph( curTitle, terms );
			graphs.add( curGraph );
		}
		clearAll();
	}
	
	// A normal graph is the other type of graph. This is used for data output that doesn't have
	// a third axis/ID. 
	public void prepareNormalGraphHeader() {
		if ( curGraph == null ) {
			curGraph = new NormalGraph( curTitle, terms );
			graphs.add( curGraph );
		}
		clearAll();
	}
	
	// Whenever a data line is finished, add this to the graph.
	public void addDataLine() {
		curGraph.addData( terms );
		clearAll();
	}
	
	// Average lines go there as well.
	public void addAverageLine() {
		curGraph.addAverage( terms );
		clearAll();
	}
	
	private void clearBuffer() {
		buffer = new StringBuilder();
	}
	
	private void clearAll() {
		clearBuffer();
		terms = new ArrayList<ITerm>();
		time = new StringBuffer();
	}
	
	/**
	 * Main entry point into the application.
	 * 
	 * @param args
	 */
    public static void main( String[] args )
    {
		GenerateSarReport main = new GenerateSarReport();
		if ( args == null ) {
			printUsage();
			return;
		}
		if ( args.length < 3 ) {
			printUsage();
			return;
		}
		main.run( args[ 0 ], args[ 1 ], args[ 2 ], args[ 3 ] );
    }
	
    /**
     * The main running function of the application.
     * 
     * For all possible invocations of sar output, parse the output text,
     * run it through the parser, let the parser call back into this class and 
     * populate each graph object.  At the very end of it, export all data.
     * 
     * @param outputDir		The directory where output is generated
     * @param sarFileName	The name of the sar file to extract text from
     * @param interval		The interval with which sar sampled the data (in seconds)
     * @param title			The title for the HTML file.
     */
	private void run( String outputDir, String sarFileName, String interval, String title ) {
		StringBuilder builder = new StringBuilder();
		Invocation inv = null;
		this.outputDir = outputDir;
		this.interval = interval;
		this.title = title;
		
		for ( int i = 0; i < invocations.size(); i++ ) {
			inv = invocations.get( i );
			
			builder = new StringBuilder();
			builder.append( "sar " );
			builder.append( inv.getCmdLine() );
			builder.append( " -f " );
			builder.append( sarFileName );

			curTitle = inv.getGraphName();
			
			System.out.println("Generating graph: " + curTitle);
			
			try {
				Process proc = Runtime.getRuntime().exec( builder.toString() );
		
				BufferedReader in = new BufferedReader( 
					new InputStreamReader( proc.getInputStream() ) );

				SarTextOutputParser stop = new SarTextOutputParser();
		
				StringBuilder buf = new StringBuilder();
				String str = "";
				while (str != null) {
					buf.append( str );
					// System.out.println( str );
					buf.append( '\n' );
					clearAll();
					if ( buf.length() > 0 ) {
						if ( ! stop.parse( this, buf.toString().toCharArray() ) ) {
							if ( buf.length() > 1 ) {
								System.out.print("Invalid line: " + buf.toString() );
							}
						}
					}
					buf = new StringBuilder();
					str = in.readLine();
				}
			} catch (IOException e) {
				e.printStackTrace();
				throw new RuntimeException( e );
			} 
		}
		exportData();
	}
	
	/**
	 * Export data runs the rrdtool three times for different purposes:
	 * 
	 *  rrdtool create:  Creates a new RoundRobin Database to store the data in.
	 *  rrdtool update:  For each line in <graph>, store the data inside the RRD DB.
	 *  rrdtool graph:   Create a PNG graph from the data.
	 *  
	 *  There's an HTML file being created along the way which simply creates "IMG" tags
	 *  that display the output in one single file.
	 * 
	 */
	private void exportData() {
		List<String> cmds = new ArrayList<String>();
		String[] cmdArray;
		StringBuilder b = new StringBuilder();
		String line;
		FileWriter fw = null;
		BufferedWriter bw = null;
		
		try {
			File htmlFile = new File( outputDir + File.separator + "results.html" );
			fw = new FileWriter( htmlFile );
			bw = new BufferedWriter( fw );
		
			bw.write( "<HTML><HEAD><TITLE>" );
			bw.write( versionInfo.getDate() );
			bw.write( " - " );
			bw.write( versionInfo.getHostName() + " ( " + versionInfo.getOsName() + " ) " );
			bw.write( versionInfo.getVersionname() + ", " );
			bw.write( versionInfo.getCpuType() );
			bw.write( "\n" );
			bw.write( "</TITLE></HEAD><BODY>\n" );
			bw.write( "<H1>" );
			bw.write( title );
			bw.write( "</H1>\n" );
			bw.write( "<H2>" );
			bw.write( versionInfo.getDate() );
			bw.write( " - " );
			bw.write( versionInfo.getHostName() + " ( " + versionInfo.getOsName() + " ) " );
			bw.write( versionInfo.getVersionname() + ", " );
			bw.write( versionInfo.getCpuType() );
			bw.write( "\n" );
			bw.write( "</H2>\n" );
			
			for ( IGraph graph: graphs ) {
				Set<String> keys = graph.getKeys();
				
				for ( String key: keys ) {
					cmds.clear();
					cmds.add( "rrdtool" );
					cmds.add( "create" );
					cmds.add( outputDir + File.separator + sanitizeFileName(graph.getName() + key) + ".rrd" );
					cmds.add( "--start" );
					cmds.add( new Long( graph.getStartTimestamp() - 1 ).toString() );
					cmds.add( "-s" );
					cmds.add( interval );
					for ( String term: graph.getColumnNames() ) {
						cmds.add( "DS:" + term + ":GAUGE:60:U:U" );
					}
					cmds.add( "RRA:AVERAGE:0.5:1:" + graph.getNumDataRows() );
					cmdArray = (String[])cmds.toArray( new String[0] );
	
					try {
						Process proc = Runtime.getRuntime().exec( cmdArray );
						int ret = proc.waitFor();
						if ( ret != 0 ) {
							System.out.println( cmds );
							System.err.println("Error creating RRD: " + ret );
						}
						// clean up if any output in stdout
						BufferedReader brCleanUp = 
							new BufferedReader(
								new InputStreamReader ( proc.getInputStream() ) );
						while ((line = brCleanUp.readLine ()) != null) {
							  System.out.println ("[Stdout] " + line);
						}
						brCleanUp.close();
						  
						// clean up if any output in stderr
						brCleanUp = 
							new BufferedReader (new InputStreamReader ( proc.getErrorStream() ));
						while ((line = brCleanUp.readLine ()) != null) {
							  System.out.println ("[Stderr] " + line);
						}
						brCleanUp.close();
					} catch (IOException e) {
						e.printStackTrace();
						throw new RuntimeException( e );
					} catch (InterruptedException e) {
						e.printStackTrace();
						throw new RuntimeException( e );
					}
				}
			}
			
			for ( IGraph graph: graphs ) {
				int rows = graph.getNumDataRows();
				int cols = graph.getNumDataCols();
				
				Set<String> keys = graph.getKeys();
				
				for ( String key: keys ) {
					for ( int i = 0; i < rows; i++ ) {
						
						cmds.clear();
						cmds.add( "rrdtool" );
						cmds.add( "update" );
						cmds.add( outputDir + File.separator + sanitizeFileName(graph.getName() + key) + ".rrd" );
						
						b = new StringBuilder();
						DateTerm dt = (DateTerm)graph.getData( key, i, 1 );
						b.append( dt.getDate().getTime() / 1000L );
	
						for ( int j = 0; j < cols; j++ ) {
							b.append( ':' );
							b.append( graph.getData( key, i, j + graph.skip() ) );
						}
	
						cmds.add( b.toString() );
						
						//System.out.println(cmds.toString());
						cmdArray = (String[])cmds.toArray( new String[ 0 ] );
						try {
							Process proc = Runtime.getRuntime().exec( cmdArray );
							int ret = proc.waitFor();
							if ( ret != 0 ) {
								System.out.println( cmds );
								System.err.println("Error updating RRD: " + ret );
							}
							// clean up if any output in stdout
							BufferedReader brCleanUp = 
								new BufferedReader(
									new InputStreamReader ( proc.getInputStream() ) );
							while ((line = brCleanUp.readLine ()) != null) {
								  System.out.println ("[Stdout] " + line);
							}
							brCleanUp.close();
							  
							// clean up if any output in stderr
							brCleanUp = 
								new BufferedReader (new InputStreamReader ( proc.getErrorStream() ));
							while ((line = brCleanUp.readLine ()) != null) {
								  System.out.println ("[Stderr] " + line);
							}
							brCleanUp.close();
						} catch (IOException e) {
							e.printStackTrace();
							throw new RuntimeException( e);
						} catch (InterruptedException e) {
							e.printStackTrace();
							throw new RuntimeException(e );
						}					
					}
				}
			}
			
			for ( IGraph graph: graphs ) {
				int cols = graph.getNumDataCols();
				
				Set<String> keys = graph.getKeys();
				
				for ( String key: keys ) {
					cmds.clear();

					cmds.add( "rrdtool" );
					cmds.add( "graph" );
					cmds.add( outputDir + File.separator + sanitizeFileName(graph.getName() + key) + ".png" );
					cmds.add( "--title=" + graph.getName() + " " + key + "" );
					cmds.add( "--start" );
					cmds.add( new Long( graph.getStartTimestamp() - 1 ).toString() );
					cmds.add( "--end" );
					cmds.add( new Long( graph.getEndTimestamp() + 1 ).toString() );
					cmds.add( "-w" );
					cmds.add( "1024" );
					cmds.add( "-h" );
					cmds.add( "768" );
					cmds.add( "-Y" );
					cmds.add( "-u" );
					cmds.add( "1.1" );
					// cmds.add( "--slope-mode" );
	
					bw.write( "<IMG SRC=\"" );
					bw.write( sanitizeFileName(graph.getName() + key) + ".png" );
					bw.write( "\"/><BR/><BR/>\n" );
					
					for ( int j = 0; j < cols; j++ ) {
						b = new StringBuilder();
						b.append( "DEF:" );
						b.append( graph.getColumnNames().get( j ) );
						b.append( "=" );
						b.append( outputDir + File.separator + sanitizeFileName(graph.getName() + key) + ".rrd" );
						b.append( ":" );
						b.append( graph.getColumnNames().get( j ) );
						b.append( ":" );
						b.append( "AVERAGE" );
						cmds.add( b.toString() );
						b = new StringBuilder();
						b.append( "LINE1:" );
						b.append( graph.getColumnNames().get( j ) );
						b.append( colors.get( j ) );
						b.append( ":" );
						b.append( graph.getColumnNames().get( j ) );
						cmds.add( b.toString() );
					}
	
					cmdArray = (String[])cmds.toArray( new String[ 0 ] );
					try {
						Process proc = Runtime.getRuntime().exec( cmdArray );
						int ret = proc.waitFor();
						if ( ret != 0 ) {
							System.out.println( cmds );
							System.err.println("Error graphing RRD: " + ret );
						}
						// clean up if any output in stdout
						BufferedReader brCleanUp = 
							new BufferedReader(
								new InputStreamReader ( proc.getInputStream() ) );
						while ((line = brCleanUp.readLine ()) != null) {
							  System.out.println ("[Stdout] " + line);
						}
						brCleanUp.close();
						  
						// clean up if any output in stderr
						brCleanUp = 
							new BufferedReader (new InputStreamReader ( proc.getErrorStream() ));
						while ((line = brCleanUp.readLine ()) != null) {
							  System.out.println ("[Stderr] " + line);
						}
						brCleanUp.close();
					} catch (IOException e) {
						e.printStackTrace();
						throw new RuntimeException( e);
					} catch (InterruptedException e) {
						e.printStackTrace();
						throw new RuntimeException(e );
					}
				}
			}
			bw.write( "</BODY></HTML>" );
			bw.flush();
			bw.close();
		} catch (FileNotFoundException e1) {
			e1.printStackTrace();
			throw new RuntimeException( e1 );
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException( e );
		}
	}
}
